/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * A type predicate that checks if a node is of a specific type.
 *
 * @typeParam T The AST node type, can be automatically inferred.
 *
 * @example
 *
 * You can also define custom types.
 *
 * ```typescript
 * import { ts } from 'qvog-dsl';
 *
 * // define a custom type
 * type ConditionalOrBinaryNode = ts.ConditionalExpression | ts.BinaryExpression;
 *
 * // define custom predicate
 * const ConditionalOrBinary: NodePredicate<ConditionalOrBinaryNode> = {
 *     is(node): node is ConditionalOrBinaryNode {
 *         return ts.isConditionalExpression(node) || ts.isBinaryExpression(node);
 *     }
 * }
 *
 * // use custom predicate
 * if (is(node, ConditionalOrBinary)) { ... }
 * ```
 *
 * @category Type
 */
export interface NodePredicate<T> {
    is(node: any): node is T;
}

/**
 * Check if an AST node is of a specific type.
 *
 * @typeParam T The AST node type, can be automatically inferred.
 *
 * @param node The AST node to check.
 * @param type Expected type of the node.
 *
 * @example
 *
 * ```typescript
 * if (is(node, BinaryExpression)) { ... }
 * ```
 *
 * @category Type
 */
export function is<T>(node: any, type: NodePredicate<T>): node is T {
    return type.is(node);
}

/**
 * Combine two node predicates using logical OR.
 *
 * @typeParam T The first AST node type, can be automatically inferred.
 * @typeParam S The second AST node type, can be automatically inferred.
 *
 * @param predicate The first node predicate.
 * @param other The second node predicate.
 *
 * @returns A new node predicate that combines the two predicates.
 *
 * @example
 *
 * You can create new node predicates by combining existing ones.
 *
 * ```typescript
 * const ConditionalOrBinary = or(ConditionalExpression, BinaryExpression);
 * ```
 *
 * It is also possible with nested `or` calls.
 *
 * ```typescript
 * const ConditionalOrBinaryOrAs = or(
 *     AsExpression,
 *     or(ConditionalExpression, BinaryExpression)
 * );
 * ```
 *
 * @category Type
 */
export function or<T, S>(predicate: NodePredicate<T>, other: NodePredicate<S>): NodePredicate<T | S> {
    return {
        is(node: any): node is T | S {
            return predicate.is(node) || other.is(node);
        },
    };
}

/**
 * Combine two node predicates using logical AND.
 *
 * @typeParam T The first AST node type, can be automatically inferred.
 * @typeParam S The second AST node type, can be automatically inferred.
 *
 * @param predicate The first node predicate.
 * @param other The second node predicate.
 *
 * @returns A new node predicate that combines the two predicates.
 *
 * @example
 *
 * You can create new node predicates by combining existing ones.
 * See {@link or} for more details.
 *
 * ```typescript
 * const ConditionalOrBinary = or(ConditionalExpression, BinaryExpression);
 * const BinaryOnly = and(ConditionalOrBinary, BinaryExpression);
 * ```
 *
 * @category Type
 */
export function and<T, S>(predicate: NodePredicate<T>, other: NodePredicate<S>): NodePredicate<T & S> {
    return {
        is(node: any): node is T & S {
            return predicate.is(node) && other.is(node);
        },
    };
}

/**
 * Negate a node predicate.
 *
 * @typeParam T The AST node type, can be automatically inferred.
 *
 * @param predicate The node predicate to negate.
 * @returns A new node predicate that negates the original one.
 *
 * @example
 *
 * ```typescript
 * const NotBinary = not(BinaryExpression);
 * ```
 *
 * @category Type
 */
export function not<T>(predicate: NodePredicate<T>): NodePredicate<T> {
    return {
        is(node: any): node is any {
            return !predicate.is(node);
        },
    };
}
